[JAVA]集合

集合类

集合跟数组一样,可以表示同样的一组元素,但是他们的相同和不同之处在于:

  • 它们都是容器,都能够容纳一组元素。

不同点:

  • 1.数组的大小是固定的,集合的大小是可变的。
  • 2.数组可以存放基本数据类型,但集合只能存放对象。

集合类的根接口

  • ArrayList的祖先就是Collection接口。

List列表

  • List列表(线性表),支持随机访问。
    List是集合类型的一个分支,它的主要特性有:
  • 是一个有序的集合,插入元素默认是插入到尾部,按顺序从前往后存放,每个元素都有一个自己的下表。
  • 列表中国运行存在重复元素。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    //List是一个有序的集合类,每个元素都有一个自己的下标位置
    //List中可插入重复元素
    //针对于这些特性,扩展了Collection接口中一些额外的操作
    public interface List<E> extends Collection<E> {
    ...

    //将给定集合中所有元素插入到当前结合的给定位置上(后面的元素就被挤到后面去了,跟我们之前顺序表的插入是一样的)
    boolean addAll(int index, Collection<? extends E> c);

    ...

    //Java 8新增方法,可以对列表中每个元素都进行处理,并将元素替换为处理之后的结果
    default void replaceAll(UnaryOperator<E> operator) {
    Objects.requireNonNull(operator);
    final ListIterator<E> li = this.listIterator(); //这里同样用到了迭代器
    while (li.hasNext()) {
    li.set(operator.apply(li.next()));
    }
    }

    //对当前集合按照给定的规则进行排序操作,这里同样只需要一个Comparator就行了
    @SuppressWarnings({"unchecked", "rawtypes"})
    default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
    i.next();
    i.set((E) e);
    }
    }

    ...

    //-------- 这些是List中独特的位置直接访问操作 --------

    //获取对应下标位置上的元素
    E get(int index);

    //直接将对应位置上的元素替换为给定元素
    E set(int index, E element);

    //在指定位置上插入元素,就跟我们之前的顺序表插入是一样的
    void add(int index, E element);

    //移除指定位置上的元素
    E remove(int index);


    //------- 这些是List中独特的搜索操作 -------

    //查询某个元素在当前列表中的第一次出现的下标位置
    int indexOf(Object o);

    //查询某个元素在当前列表中的最后一次出现的下标位置
    int lastIndexOf(Object o);


    //------- 这些是List的专用迭代器 -------

    //迭代器我们会在下一个部分讲解
    ListIterator<E> listIterator();

    //迭代器我们会在下一个部分讲解
    ListIterator<E> listIterator(int index);

    //------- 这些是List的特殊转换 -------

    //返回当前集合在指定范围内的子集
    List<E> subList(int fromIndex, int toIndex);

    ...
    }

    ArrayList

  • 在ArrayList中,底层采用数组来实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    {

    //默认的数组容量
    private static final int DEFAULT_CAPACITY = 10;

    ...

    //存放数据的底层数组,这里的transient关键字我们会在后面I/O中介绍用途
    transient Object[] elementData;

    //记录当前数组元素数的
    private int size;

    //这是ArrayList的其中一个构造方法
    public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
    this.elementData = new Object[initialCapacity]; //根据初始化大小,创建当前列表
    } else if (initialCapacity == 0) {
    this.elementData = EMPTY_ELEMENTDATA;
    } else {
    throw new IllegalArgumentException("Illegal Capacity: "+
    initialCapacity);
    }
    }

    ...

    public boolean add(E e) {
    ensureCapacityInternal(size + 1); // 这里会判断容量是否充足,不充足需要扩容
    elementData[size++] = e;
    return true;
    }

    ...

    //默认的列表最大长度为Integer.MAX_VALUE - 8
    //JVM都C++实现中,在数组的对象头中有一个_length字段,用于记录数组的长
    //度,所以这个8就是存了数组_length字段(这个只做了解就行)
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容规则跟我们之前的是一样的,也是1.5倍
    if (newCapacity - minCapacity < 0) //要是扩容之后的大小还没最小的大小大,那么直接扩容到最小的大小
    newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) //要是扩容之后比最大的大小还大,需要进行大小限制
    newCapacity = hugeCapacity(minCapacity); //调整为限制的大小
    elementData = Arrays.copyOf(elementData, newCapacity); //使用copyOf快速将内容拷贝到扩容后的新数组中并设定为新的elementData底层数组
    }
    }
  • 一般的,如果我们使用一个集合类,我们会使用接口的引用:

    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    List<String> list = new ArrayList<>(); //使用接口的引用来操作具体的集合类实现,是为了方便日后如果我们想要更换不同的集合类实现,而且接口中本身就已经定义了主要的方法,所以说没必要直接用实现类
    list.add("科技与狠活"); //使用add添加元素
    list.add("上头啊");
    System.out.println(list); //打印集合类,可以得到一个非常规范的结果
    }

  • 在使用Integer时,呀注意传参问题:

    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(10); //添加Integer的值10
    list.remove((Integer) 10); //注意,不能直接用10,默认情况下会认为传入的是int类型值,删除的是下标为10的元素,我们这里要删除的是刚刚传入的值为10的Integer对象
    System.out.println(list); //可以看到,此时元素成功被移除
    }

    LinkedList

  • LinkedList同样时List的实现类,是使用链表的。
  • 是一个双向链表,也就是同时保存两个方向:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    {
    transient int size = 0;

    //引用首结点
    transient Node<E> first;

    //引用尾结点
    transient Node<E> last;

    //构造方法,很简单,直接创建就行了
    public LinkedList() {
    }

    ...

    private static class Node<E> { //内部使用的结点类
    E item;
    Node<E> next; //不仅保存指向下一个结点的引用,还保存指向上一个结点的引用
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
    this.item = element;
    this.next = next;
    this.prev = prev;
    }
    }

    ...
    }

迭代器

通过迭代器,我们就可以实现对集合中的元素进行比阿里,就像我们遍历数组那样,运作机制大概是:

  • 一个新的迭代器就像上面这样,默认有一个指向集合中第一个元素的指针:

  • 每一次next操作,都会将指针后移译为,直到完成每一个元素的比那里。

  • 此时再调用next将不能再得到下一个元素。
  • 在调用next方法之前,迭代器的执政只想当前元素的前一个位置,而不是第一个元素。
  • 为什么要这样设计:集合类的实现方案有很多,可能是链式粗处也有可能是数组存储,不同的实现有着不同的遍历方式,而迭代器则可以将多种多样不同的集合类遍历方式进行统一,只需要各个集合类格局自己的情况进行实现就行了。

  • 迭代器接口的源码定义的操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public interface Iterator<E> {
    //看看是否还有下一个元素
    boolean hasNext();

    //遍历当前元素,并将下一个元素作为待遍历元素
    E next();

    //移除上一个被遍历的元素(某些集合不支持这种操作)
    default void remove() {
    throw new UnsupportedOperationException("remove");
    }

    //对剩下的元素进行自定义遍历操作
    default void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    while (hasNext())
    action.accept(next());
    }
    }
  • 在ArrayList和LinkedList中,迭代器的实现也不同。

  • ArrayList是直接按下标访问的:

    1
    2
    3
    4
    5
    public E next() {
    ...
    cursor = i + 1; //移动指针
    return (E) elementData[lastRet = i]; //直接返回指针所指元素
    }
  • LinkeList就是不断向后寻找节点:

    1
    2
    3
    4
    5
    6
    public E next() {
    ...
    next = next.next; //向后继续寻找结点
    nextIndex++;
    return lastReturned.item; //返回结点内部存放的元素
    }
  • 虽然这两种列表的实现不同,遍历方式也不同,但是都是按照迭代器的标准进行了实现。
  • 我们想要遍历一个集合中所有的元素,那么就可以直接使用迭代器完成,而不需要关心集合类是如何实现。
  • 注意,迭代器的使用是一次性的,用了之后就不能用了。
  • 如果需要再次进行便利操作,那么需要重写生成一个迭代器对象。

Iterable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//注意这个接口是集合接口的父接口,不要跟之前的迭代器接口搞混了
public interface Iterable<T> {
//生成当前集合的迭代器,在Collection接口中重复定义了一次
Iterator<T> iterator();

//Java8新增方法,因为是在顶层接口中定义的,因此所有的集合类都有这个方法
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}

//这个方法会在多线程部分中进行介绍,暂时不做讲解
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}

ListIterator接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public interface ListIterator<E> extends Iterator<E> {
//原本就有的
boolean hasNext();

//原本就有的
E next();

//查看前面是否有已经遍历的元素
boolean hasPrevious();

//跟next相反,这里是倒着往回遍历
E previous();

//返回下一个待遍历元素的下标
int nextIndex();

//返回上一个已遍历元素的下标
int previousIndex();

//原本就有的
void remove();

//将上一个已遍历元素修改为新的元素
void set(E e);

//在遍历过程中,插入新的元素到当前待遍历元素之前
void add(E e);
}

Queue和Deque

  • LInkList除了可以直接当做列表使用之外,还可以直接当做其他的数据结构使用。
  • 可以看到它不仅仅实现了List接口:
    1
    2
    3
    4
    public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    {
    这个Deque接口是干嘛的呢?我们先来看看它的继承结构:

Queue

  • 队列接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public interface Queue<E> extends Collection<E> {
    //队列的添加操作,是在队尾进行插入(只不过List也是一样的,默认都是尾插)
    //如果插入失败,会直接抛出异常
    boolean add(E e);

    //同样是添加操作,但是插入失败不会抛出异常
    boolean offer(E e);

    //移除队首元素,但是如果队列已经为空,那么会抛出异常
    E remove();

    //同样是移除队首元素,但是如果队列为空,会返回null
    E poll();

    //仅获取队首元素,不进行出队操作,但是如果队列已经为空,那么会抛出异常
    E element();

    //同样是仅获取队首元素,但是如果队列为空,会返回null
    E peek();
    }

Dueque

  • 双端队列就是队列的升级版,我们一个普通的队列就是:

  • 普通队列中从队尾入队,队首出队,而双端队列允许在队列的两端进行入队和出队操作:

  • 用这种特性,双端队列既可以当做普通队列使用,也可以当做栈来使用,我们来看看Java中是如何定义的Deque双端队列接口的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    //在双端队列中,所有的操作都有分别对应队首和队尾的
    public interface Deque<E> extends Queue<E> {
    //在队首进行插入操作
    void addFirst(E e);

    //在队尾进行插入操作
    void addLast(E e);

    //不用多说了吧?
    boolean offerFirst(E e);
    boolean offerLast(E e);

    //在队首进行移除操作
    E removeFirst();

    //在队尾进行移除操作
    E removeLast();

    //不用多说了吧?
    E pollFirst();
    E pollLast();

    //获取队首元素
    E getFirst();

    //获取队尾元素
    E getLast();

    //不用多说了吧?
    E peekFirst();
    E peekLast();

    //从队列中删除第一个出现的指定元素
    boolean removeFirstOccurrence(Object o);

    //从队列中删除最后一个出现的指定元素
    boolean removeLastOccurrence(Object o);

    // *** 队列中继承下来的方法操作是一样的,这里就不列出了 ***

    ...

    // *** 栈相关操作已经帮助我们定义好了 ***

    //将元素推向栈顶
    void push(E e);

    //将元素从栈顶出栈
    E pop();


    // *** 集合类中继承的方法这里也不多种介绍了 ***

    ...

    //生成反向迭代器,这个迭代器也是单向的,但是是next方法是从后往前进行遍历的
    Iterator<E> descendingIterator();

    }

SET

定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public interface Set<E> extends Collection<E> {
// Set集合中基本都是从Collection直接继承过来的方法,只不过对这些方法有更加特殊的定义
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);

//添加元素只有在当前Set集合中不存在此元素时才会成功,如果插入重复元素,那么会失败
boolean add(E e);

//这个同样是删除指定元素
boolean remove(Object o);

boolean containsAll(Collection<?> c);

//同样是只能插入那些不重复的元素
boolean addAll(Collection<? extends E> c);

boolean retainAll(Collection<?> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();

//这个方法我们同样会放到多线程中进行介绍
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
}

  • 不允许出现重复元素。
  • 不支持随机访问。

    HashSET

  • 底层是采用哈希表实现的。
  • 我们无法维持插入元素的顺序。
  • 需要维护顺序则使用LinkedHasSet。
  • 排序使用TreeSet。
    内部维护了一个HashMAP。

MAP

  • MAP接口中的一些操作
    //Map并不是Collection体系下的接口,而是单独的一个体系,因为操作特殊
    //这里需要填写两个泛型参数,其中K就是键的类型,V就是值的类型,比如上面的学生信息,ID一般是int,那么键就是Integer类型的,而值就是学生信息,所以说值是学生对象类型的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    public interface Map<K,V> {
    //-------- 查询相关操作 --------

    //获取当前存储的键值对数量
    int size();

    //是否为空
    boolean isEmpty();

    //查看Map中是否包含指定的键
    boolean containsKey(Object key);

    //查看Map中是否包含指定的值
    boolean containsValue(Object value);

    //通过给定的键,返回其映射的值
    V get(Object key);

    //-------- 修改相关操作 --------

    //向Map中添加新的映射关系,也就是新的键值对
    V put(K key, V value);

    //根据给定的键,移除其映射关系,也就是移除对应的键值对
    V remove(Object key);


    //-------- 批量操作 --------

    //将另一个Map中的所有键值对添加到当前Map中
    void putAll(Map<? extends K, ? extends V> m);

    //清空整个Map
    void clear();


    //-------- 其他视图操作 --------

    //返回Map中存放的所有键,以Set形式返回
    Set<K> keySet();

    //返回Map中存放的所有值
    Collection<V> values();

    //返回所有的键值对,这里用的是内部类Entry在表示
    Set<Map.Entry<K, V>> entrySet();

    //这个是内部接口Entry,表示一个键值对
    interface Entry<K,V> {
    //获取键值对的键
    K getKey();

    //获取键值对的值
    V getValue();

    //修改键值对的值
    V setValue(V value);

    //判断两个键值对是否相等
    boolean equals(Object o);

    //返回当前键值对的哈希值
    int hashCode();

    ...
    }

    ...
    }
  • MAP总共无法添加相同的键,同样的键只能存在一个,即使值不同。如果出现键相同的情况,那么会覆盖掉之前的。

  • HaspMAP底层使用哈希表来实现。不维护顺序,我们在获取所有键和所有值时,可能会是乱序的。
  • 如果需要维护顺序,可以使用LinkedHashMap,它的内部对插入顺序进行了维护。

    HashMAP

  • 底层:
  • 实际上这个表就是一个存放头结点的数组+若干结点,而HashMap也是这样的,我们来看看这里面是怎么定义的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
        public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    ...

    static class Node<K,V> implements Map.Entry<K,V> { //内部使用结点,实际上就是存放的映射关系
    final int hash;
    final K key; //跟我们之前不一样,我们之前一个结点只有键,而这里的结点既存放键也存放值,当然计算哈希还是使用键
    V value;
    Node<K,V> next;
    ...
    }

    ...

    transient Node<K,V>[] table; //这个就是哈希表本体了,可以看到跟我们之前的写法是一样的,也是头结点数组,只不过HashMap中没有设计头结点(相当于没有头结点的链表)

    final float loadFactor; //负载因子,这个东西决定了HashMap的扩容效果

    public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; //当我们创建对象时,会使用默认的负载因子,值为0.75
    }

    ...
    }
  • 可以看到,实际上底层大致结构跟我们之前学习的差不多,只不过多了一些特殊的东西:

    • HashMap支持自动扩容,哈希表的大小并不是一直不变的,否则太过死板
    • HashMap并不是只使用简单的链地址法,当链表长度到达一定限制时,会转变为效率更高的红黑树结构
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
          public V put(K key, V value) {
      //这里计算完键的哈希值之后,调用的另一个方法进行映射关系存放
      return putVal(hash(key), key, value, false, true);
      }

      final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
      boolean evict) {
      Node<K,V>[] tab; Node<K,V> p; int n, i;
      if ((tab = table) == null || (n = tab.length) == 0) //如果底层哈希表没初始化,先初始化
      n = (tab = resize()).length; //通过resize方法初始化底层哈希表,初始容量为16,后续会根据情况扩容,底层哈希表的长度永远是2的n次方
      //因为传入的哈希值可能会很大,这里同样是进行取余操作
      //(n - 1) & hash 等价于 hash % n 这里的i就是最终得到的下标位置了
      if ((p = tab[i = (n - 1) & hash]) == null)
      tab[i] = newNode(hash, key, value, null); //如果这个位置上什么都没有,那就直接放一个新的结点
      else { //这种情况就是哈希冲突了
      Node<K,V> e; K k;
      if (p.hash == hash && //如果上来第一个结点的键的哈希值跟当前插入的键的哈希值相同,键也相同,说明已经存放了相同键的键值对了,那就执行覆盖操作
      ((k = p.key) == key || (key != null && key.equals(k))))
      e = p; //这里直接将待插入结点等于原本冲突的结点,一会直接覆盖
      else if (p instanceof TreeNode) //如果第一个结点是TreeNode类型的,说明这个链表已经升级为红黑树了
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //在红黑树中插入新的结点
      else {
      for (int binCount = 0; ; ++binCount) { //普通链表就直接在链表尾部插入
      if ((e = p.next) == null) {
      p.next = newNode(hash, key, value, null); //找到尾部,直接创建新的结点连在后面
      if (binCount >= TREEIFY_THRESHOLD - 1) //如果当前链表的长度已经很长了,达到了阈值
      treeifyBin(tab, hash); //那么就转换为红黑树来存放
      break; //直接结束
      }
      if (e.hash == hash &&
      ((k = e.key) == key || (key != null && key.equals(k)))) //同样的,如果在向下找的过程中发现已经存在相同键的键值对了,直接结束,让p等于e一会覆盖就行了
      break;
      p = e;
      }
      }
      if (e != null) { // 如果e不为空,只有可能是前面出现了相同键的情况,其他情况e都是null,所有直接覆盖就行
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
      e.value = value;
      afterNodeAccess(e);
      return oldValue; //覆盖之后,会返回原本的被覆盖值
      }
      }
      ++modCount;
      if (++size > threshold) //键值对size计数自增,如果超过阈值,会对底层哈希表数组进行扩容
      resize(); //调用resize进行扩容
      afterNodeInsertion(evict);
      return null; //正常插入键值对返回值为null
      }

      - 如果底层哈希表没有初始化,先初始化。
      - 默认长度为16,后会扩容,底层哈希表的长度永远为2的n次方。
      - 如果已经初始化,取余存值。
      - 哈希冲突:哈希值相同。
      - 如果键相同,直接覆盖。
      - 键不相同,链表插入。
      - 链表超过8,变成红黑树。
      - 红黑树插入。
      - 节点数量超过阈值,对哈希数组进行扩容。通过resize()进行扩容。
      final Node<K,V>[] resize() {
      Node<K,V>[] oldTab = table; //先把下面这几个旧的东西保存一下
      int oldCap = (oldTab == null) ? 0 : oldTab.length;
      int oldThr = threshold;
      int newCap, newThr = 0; //这些是新的容量和扩容阈值
      if (oldCap > 0) { //如果旧容量大于0,那么就开始扩容
      if (oldCap >= MAXIMUM_CAPACITY) { //如果旧的容量已经大于最大限制了,那么直接给到 Integer.MAX_VALUE
      threshold = Integer.MAX_VALUE;
      return oldTab; //这种情况不用扩了
      }
      else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
      oldCap >= DEFAULT_INITIAL_CAPACITY) //新的容量等于旧容量的2倍,同样不能超过最大值
      newThr = oldThr << 1; //新的阈值也提升到原来的两倍
      }
      else if (oldThr > 0) // 旧容量不大于0只可能是还没初始化,这个时候如果阈值大于0,直接将新的容量变成旧的阈值
      newCap = oldThr;
      else { // 默认情况下阈值也是0,也就是我们刚刚无参new出来的时候
      newCap = DEFAULT_INITIAL_CAPACITY; //新的容量直接等于默认容量16
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //阈值为负载因子乘以默认容量,负载因子默认为0.75,也就是说只要整个哈希表用了75%的容量,那么就进行扩容,至于为什么默认是0.75,原因很多,这里就不解释了,反正作为新手,这些都是大佬写出来的,我们用就完事。
      }
      ...
      threshold = newThr;
      @SuppressWarnings({"rawtypes","unchecked"})
      Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
      table = newTab; //将底层数组变成新的扩容之后的数组
      if (oldTab != null) { //如果旧的数组不为空,那么还需要将旧的数组中所有元素全部搬到新的里面去
      ... //详细过程就不介绍了
      }
      }

LinkHaspMAP

  • 维护插入顺序。
  • 继承自HahpMAP。具有HashMap的全部性质,同时得益于每一个节点都是一个双向链表,在插入键值对时,同时保存了插入顺序。
    1
    2
    3
    4
    5
    6
        static class Entry<K,V> extends HashMap.Node<K,V> {   //LinkedHashMap中的结点实现
    Entry<K,V> before, after; //这里多了一个指向前一个结点和后一个结点的引用
    Entry(int hash, K key, V value, Node<K,V> next) {
    super(hash, key, value, next);
    }
    }

TreeMAP

  • ,它的内部直接维护了一个红黑树(没有使用哈希表)因为它会将我们插入的结点按照规则进行排序,所以说直接采用红黑树会更好,我们在创建时,直接给予一个比较规则即可,跟之前的TreeSet是一样的:
    1
    2
    3
    4
    5
    6
    7
        public static void main(String[] args) {
    Map<Integer , String> map = new TreeMap<>((a, b) -> b - a);
    map.put(0, "单走");
    map.put(1, "一个六");
    map.put(3, "**");
    System.out.println(map);
    }